# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

"""Rules to describe OpenTitan HW"""

load("//rules:host.bzl", "host_tools_transition")

def opentitan_ip(name, files = {}, **kwargs):
    """
    Return a structure describing an IP. This can be given to opentitan_top.

    Example:
    ```
    opentitan_ip(
        name = "pwrmgr",
        files = {
            'hjson': "//hw/top_earlgrey/ip_autogen/pwrmgr:data/pwrmgr.hjson",
            'ipconfig': "//hw/top_earlgrey/ip_autogen/pwrmgr:data/top_earlgrey_pwrmgr.ipconfig.hjson",
        },
    )
    ```

    Arguments:
    - name: name of ip in lower case.
    - files: a map from strings to labels, you MUST NOT use a relative label.
    - kwargs: for backward compatibility, any key listed here will be treated as a key in `files`
    """
    return struct(
        name = name,
        files = files | kwargs,
    )

def opentitan_top(name, hjson, top_lib, top_ld, ips):
    """
    Return a structure describing a top.

    Arguments:
    - name: name of top in lower case.
    - hjson: label of the top's hjson path (generated by topgen), you MUST NOT
             use a relative label.
    - top_lib: same but for the top's library.
    - top_ld: same but for the top's linker script.
    - ips: array of ips, the entries must be built by opentitan_ip().
    """
    return struct(
        name = name,
        hjson = hjson,
        top_lib = top_lib,
        top_ld = top_ld,
        ips = ips,
    )

OpenTitanTopInfo = provider(
    doc = "Information about an OpenTitan top",
    fields = {
        "name": "Name of this top (string)",
        "hjson": "topgen-generated HJSON file for this top (file)",
        "ip_map": "dictionary of IPs files (dict: {ipname (string): {attr (string): file}})",
    },
)

def opentitan_top_get_ip_attr(
        top,
        ipname,
        attr,
        required = True,
        default = None,
        output = "file"):
    """
    Return an IP's attribute (e.g. 'hjson'). If `required` is set, this function will
    throw an error when the attribute is not present. Otherwise, it will return `default`.
    In all cases, the function will throw an error if the IP is not present in the top.

    Arguments:
    - top: an `OpenTitanTopInfo` provider.
    - ipname: name of the IP.
    - attr: name of the attribute.
    - required: whether the attribute is required.
    - output: specifies the output type (see below).
    - default: return value if `required` is false and the attribute is not present.

    The output type specifies what kind of processing is done on the target:
    - "files": will return the DefaultInfo as a list of files
    - "file": same as file but return a single file, error if there are more
    - "target": return the raw bazel target
    """
    if ipname not in top.ip_map:
        fail("top {} does not contain IP {}".format(top.name, ipname))
    ip_files = top.ip_map[ipname]
    if required and (attr not in ip_files):
        fail("top {} does not contain attribute '{}' for IP {}".format(top.name, attr, ipname))

    target = ip_files.get(attr, None)
    if target == None:
        return default
    if output in ["file", "files"]:
        files = target[DefaultInfo].files.to_list()
        if output == "file" and len(files) != 1:
            fail("IP {} in top {} provide several files for attribute {} but only one requested"
                .format(ipname, top.name, attr))

        return files[0] if output == "file" else files
    else:
        return target

def _describe_top(ctx):
    ip_map = {}

    # We cannot use ctx.files because it is only a list and not a dict.
    for (encoded, files) in ctx.attr.ip_map.items():
        ipname, attr = encoded.split("/", 1)

        if ipname not in ip_map:
            ip_map[ipname] = {}
        ip_map[ipname][attr] = files

    return [
        OpenTitanTopInfo(
            name = ctx.attr.topname,
            hjson = ctx.file.hjson,
            ip_map = ip_map,
        ),
    ]

describe_top_rule = rule(
    implementation = _describe_top,
    doc = """Create a target that provides the description of a top in the form of an OpenTitanTopInfo provider.""",
    attrs = {
        "hjson": attr.label(mandatory = True, allow_single_file = True, doc = "toplevel hjson file generated by topgen"),
        "ip_map": attr.string_keyed_label_dict(
            allow_files = True,
            # Transition to host because some of those attributes could use targets (e.g. python libraries) that only work
            # on the host platform.
            cfg = host_tools_transition,
            doc = "mapping from '<ipname>/<attr>' to targets",
        ),
        "topname": attr.string(mandatory = True, doc = "Name of the top"),
    },
)

def describe_top(name, all_tops, top):
    """
    Create a target that provides an OpenTitanTopInfo corresponding to the
    requested top.

    - all_tops: list of tops (created by opentitan_top).
    - top: name of the top to use.
    """

    # Although we already pass the top description to the rule, those are just strings.
    # We also need to let bazel know that we depend on the hjson files which is why
    # we also pass them in a structured way.
    ip_map = {}
    top_hjson = None
    for _top in all_tops:
        if _top.name != top:
            continue
        top_hjson = _top.hjson
        for ip in _top.ips:
            for (attr, files) in ip.files.items():
                ip_map["{}/{}".format(ip.name, attr)] = files

    if top_hjson == None:
        fail("top {} not found in the provided list of tops".format(top))

    describe_top_rule(
        name = name,
        hjson = top_hjson,
        ip_map = ip_map,
        topname = top,
    )

def select_top_lib(name, all_tops, top):
    """
    Create an alias to the top library.
    """
    libs = [_top.top_lib for _top in all_tops if _top.name == top]
    if len(libs) == 0:
        fail("not top found with name {}".format(top))

    native.alias(
        name = name,
        actual = libs[0],
    )

def select_top_ld(name, all_tops, top):
    """
    Create an alias to the top library.
    """
    libs = [_top.top_ld for _top in all_tops if _top.name == top]
    if len(libs) == 0:
        fail("not top found with name {}".format(top))

    native.alias(
        name = name,
        actual = libs[0],
    )
